package net.ib.mtalk.service;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import net.ib.mntalk.protocol.Common;
import net.ib.mntalk.protocol.Common.NotificationId;
import net.ib.mntalk.protocol.Common.NotificationInfo;
import net.ib.mntalk.protocol.Session.PongRes;
import net.ib.mntalk.protocol.Session.SelectReq;
import net.ib.mntalk.protocol.Session.SelectRes;
import net.ib.mtalk.constant.MTalkConstant;
import net.ib.mtalk.constant.NetworkConstant;
import net.ib.mtalk.global.GlobalPreferences;
import net.ib.mtalk.network.NetFlowControl;
import net.ib.mtalk.network.NetResultReceiver;
import net.ib.mtalk.network.TcpClient;
import net.ib.mtalk.network.TcpEnvelope;
import net.ib.mtalk.network.serverapi.Notification;
import net.ib.mtalk.network.serverapi.Ping;
import net.ib.mtalk.util.GpbSupport;
import net.ib.mtalk.util.MTalkLogWriter;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.util.Log;

/*
 * <b>서비스의 역할의 개편</b>
 * 
 * 이제 서비스가 모든 select를 다 관리해야 한다.
 * 
 * UI의 통보에 따라 mode를 바꾸고, 그에 따라 select 항목을 어떻게 걸 것인지 결정되어야 한다.
 * 그런데, 서비스는 임의 시점에 OS에 의해 종료당할 수 있다. 이 때 멤버 변수는 초기화되는 것으로 봐야 하며,
 * mode 정보 역시 서비스가 종료되어도 살아남는 영역에 저장되어 있어서, 임의 시점에 읽을 수 있어야 한다.
 * mode 기본값은 1셀렉트(푸시를 위한 것을 말함. 반대는 4셀렉트라고 부르자)를 허용한다는 의미여야 한다.
 * 
 * 각 셀렉트 결과에 대한 부속 처리는 서비스 내부에서 할 것이 아니라 각각 별도의 task 연쇄가 처리하도록 해야 한다.
 * 
 * 현재 서비스가 단독 스레드를 가지고 있는데 이것은 유지해야 하는가?
 * 분기한 task는 서비스 스레드가 아닌 task용 스레드에서 돌아갈 것이다.
 * ------ 유지해야 할 것이다. 어쨌든 메인 스레드는 ANR로부터 완전히 자유로워야 한다.
 * 
 * select 재호출은 언제 이루어져야 하는가?
 * task 말단에서 이루어진다면 논리상 명백하나 유지보수에 위험성을 안게 된다.
 * 그렇다고 select 리턴 시점에 서비스가 즉시 재호출을 결정하게 되면 노티 아이디를 안전하게 결정할 수 없다.
 * ------ 논리상 task 말단에서 해야 할 것 같다.
 * */
/**
 * {@link #setMode(Context, int)} 등의 static public인 메소드나 상수를 이용하여 서비스 상태 제어를 할 수 있다. <br>
 * <br>
 * 단말기 상황이나 가진 정보에 대한 판단은 외부의 지시 없이 자체적으로 이루어진다.
 * 예를 들어 C2DM을 쓸 수 있는 단말이라면 1-select 플로우에서도 아무 일도 하지 않는다.
 * @author Arngard
 *
 */
public class SelecterService extends BaseService {

	/*	WakeLock 시작	*/

	private PowerManager.WakeLock mWakeLock;

	/*	WakeLock 끝	*/

	/*	인텐트 액션 시작	*/

	private static final Lock mActionLock = new ReentrantLock();

	static final String ACTION_BIND = "net.ib.mtalk.service.BIND";

	static final String ACTION_STOP = "net.ib.mtalk.service.STOP";
	static final String ACTION_START = "net.ib.mtalk.service.START";
	static final String ACTION_PUSH = "net.ib.mtalk.service.PUSH";
	static final String ACTION_CHECK = "net.ib.mtalk.service.CHECK";

	static final String ACTION_TRAFFIC = "net.ib.mtalk.service.TRAFFIC";
	static final String ALARM_REGIST_TIME = "regTime";
	static final String ACTION_COLLECT = "net.ib.mtalk.service.COLLECTOR";

	/**
	 * {@link Context#bindService(Intent, ServiceConnection, int)} 대신 사용하는 래퍼.
	 * @param context 인텐트 생성과 startService()에 쓰일 객체
	 * @param connection 바인드 콜백
	 */
	public static void actionBind(Context context, ServiceConnection connection) {
		mActionLock.lock();
		try {
			if (context == null) return;
			if (connection == null) return;
			Intent i = new Intent(context, SelecterService.class);
			i.setAction(ACTION_BIND);
			context.bindService(i, connection, Context.BIND_AUTO_CREATE);
		} finally {
			mActionLock.unlock();
		}
	}

	/**
	 * Whether or not, service is alive.
	 * @param context
	 * @return true if service is alive.
	 */
	public static boolean isRunning(Context context) throws SecurityException {
		mActionLock.lock();
		try {
			return nonLockIsRunning(context);
		} finally {
			mActionLock.unlock();
		}
	}

	private static boolean nonLockIsRunning(Context context) throws SecurityException {
		int searchCapacity = 50;
		ActivityManager actmgr = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
		for(ActivityManager.RunningServiceInfo serviceInfo : actmgr.getRunningServices(searchCapacity)) {
			if (SelecterService.class.getName().equals(serviceInfo.service.getClassName())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 예약된 인텐트를 포함하여 일체의 진행중인 동작을 중단하도록 요청한다.
	 * 서비스 자체가 종료된다는 뜻은 아니다.
	 * @param context 인텐트 생성과 startService()에 쓰일 객체
	 */
	public static void actionStop(Context context) {
		mActionLock.lock();
		try {
			if (context == null) return;
			Intent i = new Intent(context, SelecterService.class);
			i.setAction(ACTION_STOP);
			context.startService(i);
		} finally {
			mActionLock.unlock();
		}
	}

	/**
	 * 중복해서 실행하는 경우는 엄밀히 고려되어 있지 않다.
	 * 그러므로 단순히 죽어있는 서비스를 살리기 위한 경우에는
	 *  {@link SelecterService#checkAndStart(Context) checkAndStart(Context)}를 사용할 것을 권장한다.
	 * @param context 인텐트 생성과 startService()에 쓰일 객체
	 * @throws SecurityException
	 */
	public static void actionStart(Context context) throws SecurityException {
		mActionLock.lock();
		try {
			nonLockActionStart(context);
		} finally {
			mActionLock.unlock();
		}
	}

	private static void nonLockActionStart(Context context) throws SecurityException {
		if (context == null) return;
		Intent i = new Intent(context, SelecterService.class);
		i.setAction(ACTION_START);
		context.startService(i);
	}

	/**
	 * 이 서비스가 실행되고 있지 않다면 실행한다.
	 * @param context 인텐트 생성과 startService()에 쓰일 객체
	 * @return true if service was not alive and started to start. false if already running or start intent failed.
	 */
	public static synchronized boolean checkAndStart(Context context) {
		mActionLock.lock();
		try {
			if (!nonLockIsRunning(context)) {
				nonLockActionStart(context);
				return true;
			}
			return false;
		} catch(Exception e) {
			MTalkLogWriter.e(e);
			return false;
		} finally {
			mActionLock.unlock();
		}
	}

	/**
	 * C2DM 푸시가 왔을 때
	 * @param context 인텐트 생성과 startService()에 쓰일 객체
	 * @throws SecurityException
	 */
	public static void actionPush(Context context) throws SecurityException {
		mActionLock.lock();
		try {
			if (context == null) return;
			Intent i = new Intent(context, SelecterService.class);
			i.setAction(ACTION_PUSH);
			context.startService(i);
		} finally {
			mActionLock.unlock();
		}
	}

	/**
	 * 네트워크 트래픽이 발생할 때 호출된다.
	 * @param context 인텐트 생성과 startService()에 쓰일 객체
	 * @throws SecurityException
	 */
	public static void actionTraffic(Context context) throws SecurityException {
		mActionLock.lock();
		try {
			if (context == null) return;
			Intent i = new Intent(context, SelecterService.class);
			i.setAction(ACTION_TRAFFIC);
			context.startService(i);
		} finally {
			mActionLock.unlock();
		}
	}

	/**
	 * 네트워크 트래픽이 없을 때 호출된다. {@link #actionTraffic(Context)}에 의해 밀려난다.
	 * @param context 인텐트 생성과 startService()에 쓰일 객체
	 * @throws SecurityException
	 */
	public static void actionCollect(Context context) throws SecurityException {
		mActionLock.lock();
		try {
			if (context == null) return;
			Intent i = new Intent(context, SelecterService.class);
			i.setAction(ACTION_COLLECT);
			context.startService(i);
		} finally {
			mActionLock.unlock();
		}
	}
	
	/*	인텐트 액션 끝	*/

	/*	생명주기 대응 시작	*/

	@Override
	public void onCreate() {
		super.onCreate();	// 반드시 호출해야 함.

		startWorker();

		PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
		mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, MTalkConstant.TAG+"_Main");

		onPhaseFinish();
	}

	/**
	 * onStart(...) 또는 onStartCommand(...) 에서 할 작업을 여기에 구현하십시오.
	 */
	@Override
	protected void handleStart(Intent intent, int startId) {
		// PusherService는 요청자의 스레드에서 동작한다.
		// 요청자를 block하지 않기 위해 작업의 내용을 Worker 스레드에 넘겨주자.
		// 넘겨준 정보는 Worker 스레드가 onWorkerRequest(Message)에서 받을 것이다.
		try {
			Message msg = getWorkerHandler().obtainMessage();
			msg.what = startId;
			msg.obj = intent;
			getWorkerHandler().sendMessage(msg);
		} catch (Exception e) {
			MTalkLogWriter.e(e);
		}
	}

	@Override
	public void onDestroy() {
		super.onDestroy();

		endWorker();
	}

	/*	생명주기 대응 끝	*/

	/*	바인딩 시작	*/

	/**
	 * {@link SelecterService#onBind(Intent)}에 리턴될 Binder의 클래스
	 * @author namkhoh
	 */
	public class LocalBinder extends Binder {
		public SelecterService getService() {
			return SelecterService.this;
		}
	}

	/**
	 * {@link SelecterService#onBind(Intent)}에서 리턴할 객체
	 */
	private final IBinder mBinder = new LocalBinder();

	/**
	 * 실제로는 {@link LocalBinder} 객체를 리턴.
	 * */
	@Override
	public IBinder onBind(Intent intent) {
		return mBinder;
	}

	/*	바인딩 끝	*/

	/*	AlarmManager 관리 시작	*/

	protected static void scheduleWork(Context ctx, String action, long delay) {
		stopSchedule(ctx, action);

		long now = System.currentTimeMillis();

		MTalkLogWriter.v("SelecterService: Scheduling Action: " + action + ", delay: " + (delay/1000) + "sec");
		Intent i = new Intent();
		i.setClass(ctx, SelecterService.class);
		i.setAction(action);
		i.putExtra(ALARM_REGIST_TIME, android.os.SystemClock.elapsedRealtime());
		PendingIntent pi = PendingIntent.getService(ctx, 0, i, 0);
		AlarmManager alarmMgr = (AlarmManager)ctx.getSystemService(ALARM_SERVICE);
		alarmMgr.set(AlarmManager.RTC_WAKEUP, now + delay, pi);
	}

	protected static void scheduleRepeating(Context ctx, String action, long interval) {
		stopSchedule(ctx, action);

		MTalkLogWriter.v("SelecterService: Starting Action: " + action + ", interval: " + (interval/1000) + "sec");
		long now = System.currentTimeMillis();

		Intent i = new Intent();
		i.setClass(ctx, SelecterService.class);
		i.setAction(action);
		PendingIntent pi = PendingIntent.getService(ctx, 0, i, 0);
		AlarmManager alarmMgr = (AlarmManager)ctx.getSystemService(ALARM_SERVICE);
		alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, 
				now + interval, interval, pi);
	}

	protected static void stopSchedule(Context ctx, String action) {
		MTalkLogWriter.v("SelecterService: Stopping Action: " + action);

		Intent i = new Intent();
		i.setClass(ctx, SelecterService.class);
		i.setAction(action);
		PendingIntent pi = PendingIntent.getService(ctx, 0, i, 0);
		AlarmManager alarmMgr = (AlarmManager)ctx.getSystemService(ALARM_SERVICE);
		alarmMgr.cancel(pi);
	}

	/*	AlarmManager 관리 끝	*/

	/*	조건 관리 시작	*/

	public enum State {
		/** 서비스 작업이 정지되어 있는 상태 */
		INACTIVE,
		/** 서비스 작업이 진행중인 상태 */
		ACTIVE,
		/** 네트워크 문제로 장시간 대기중인 상태 */
		LONGWAIT
	};
	protected static State currentState = State.INACTIVE;

	/**
	 * @return 서비스의 현재 상태
	 * @see #setState(int)
	 */
	public static State getState() {
		MTalkLogWriter.v("SelecterService.setState(): current state is "+currentState.name());
		return currentState;
	}

	/**
	 * @param state 새로 세팅할 상태
	 * @return 변했다면 true
	 */
	public static boolean setState(State newState) {
		if (currentState == newState) {
			return false;
		}
		currentState = newState;
		MTalkLogWriter.d("SelecterService.setState(): state changed to "+newState.name());
		return true;
	}

	public enum Mode {
		/**
		 * 기본 플로우 상태. 푸시를 위한 대기 동작을 하는 상태를 말한다. C2DM을 수신할 수 있는 상태라면 아무 것도 하지 않는다.
		 * 만일 C2DM이 등록되지 않았거나 사용할 수 없는 단말인 경우 View changed 셀렉트를 걸고 푸시 동작을 모사한다. 
		 */
		MONO_SELECT,
		/** 아무 동작도 하지 않는다. */
		NONE_SELECT,
		/** 4개 셀렉트를 모두 걸고 결과를 모두 처리한다. */
		FULL_SELECT
	}
	protected static Mode currentMode = Mode.MONO_SELECT;

	/**
	 * @return 현재 지정되어 있는 서비스의 행동 양식
	 * @see #setMode(Context, Mode)
	 */
	public static Mode getMode() {
		MTalkLogWriter.v("SelecterService.getMode(): current mode is "+currentMode.name());
		return currentMode;
	}

	/**
	 * 이 메소드의 호출은 플래그만 바꾸는 게 아니다.
	 * 모드가 바뀐 경우, 서비스의 현재 동작을 중단하고 새로 지정된 행동 양식에 맞는 동작을 하게 만든다.
	 * @param context 인텐트 생성과 startService()에 쓰일 객체
	 * @param condition 새로이 지정할 서비스의 행동 양식
	 * @return 변했다면 true
	 * @see #getMode()
	 */
	public static boolean setMode(Context context, Mode condition) {
		if (currentMode == condition) {
			return false;
		}
		// 이 메소드의 리턴이 true로 결정되는 상황에서, 진행중이던 셀렉트를 중단하고 새로 다시 해야 한다.
		actionStop(context);
		currentMode = condition;
		MTalkLogWriter.d("SelecterService.setMode(): mode changed to "+currentMode.name());
		actionStart(context);

		return true;
	}

	/*	조건 관리 끝	*/

	/*	Worker Thread 시작	*/

	private final Lock mWorkerLock = new ReentrantLock();

	/**
	 * WorkerThread에서 수행되며 주 작업을 수행한다.
	 */
	@Override
	protected void onWorkerRequest(Message msg) {
		System.gc();

		mWorkerLock.lock();
		if (!mWakeLock.isHeld()) {
			mWakeLock.acquire();
		}
		try {
			final Intent intent = (Intent) msg.obj;
			//final int startId = msg.what;
			String actionFromIntent = null;

			// API 2.0 이상에서 서비스가 OS에 의해 강제로 재시작하는 경우 null 인텐트로 onStartCommand()가 호출된다.
			// 이에 대해 ACTION_START와 같은 대응을 해 줘야 함.
			if (intent == null) {
				MTalkLogWriter.v("SelecterService.onWorkerRequest() - start by null Intent");
				actionFromIntent = ACTION_START;
			} else {
				actionFromIntent = intent.getAction();
			}

			// 인텐트 내용에 따른 분기
			if (ACTION_STOP.equals(actionFromIntent)) {
				onPhaseStop();
				onPhaseFinish();
			} else if (ACTION_START.equals(actionFromIntent)) {
				onPhaseStart();
			} else if (ACTION_PUSH.equals(actionFromIntent)) {
				onPhasePush();
			} else if (ACTION_CHECK.equals(actionFromIntent)) {
				onPhaseCheck();
			}
			// 이하 분기는 셀렉트 관련이 아님.
			else if (ACTION_TRAFFIC.equals(actionFromIntent)) {
				onNetworkTraffic();
			} else if (ACTION_COLLECT.equals(actionFromIntent)) {
				onNetworkCollect(intent.getLongExtra(ALARM_REGIST_TIME, 0));
			}
		} finally {
			if (mWakeLock.isHeld()) {
				mWakeLock.release();
			}
			mWorkerLock.unlock();
		}
	}

	/*	Worker Thread 끝	*/

	/*	네트워크 결과에 따른 페이즈 분기 시작	*/

	private NetResultReceiver mCallback = null;

	/**
	 * @return 리시버 (있으면 없애고) 새로 만들어 줌
	 */
	private NetResultReceiver makeNewCallback() {
		if (mCallback != null) {	// 리시버가 있으면
			mCallback.recall();	// 리시버를 무효화하고
			mCallback = null;	// 내가 들고 있는 참조를 버린다.
		}
		mCallback = new NetResultReceiver(mHandler, new ServiceCallback());	// 준비됐으니 새 리시버를 만들어준다.
		return mCallback;
	}

	/**
	 * @return 리시버 있으면 그냥 주고, 없으면 만들어 줌.
	 */
	protected NetResultReceiver getCallback() {
		if (mCallback == null || mCallback.isRecalled()) {
			makeNewCallback();
		}
		return mCallback;
	}

	class ServiceCallback implements NetResultReceiver.Receiver {

		@Override
		public void onNetResult(int resultCode, Object sentObject, Object receivedObject) {
			if (resultCode != NetResultReceiver.TCP_RESULT) {	// 그냥 예외 체크
				MTalkLogWriter.e("SelecterService.ServiceCallback.onNetResult(): Should not reach here. It's not tcp result");
				return;
			}
			try {
				TcpEnvelope recvEnvelope = (TcpEnvelope) receivedObject;
				// 네트워크 결과에 따른 분기
				switch (recvEnvelope.getMethodId()) {
				case net.ib.mtalk.network.MethodId.Session.Ping.Ping:
					onPingResult((PongRes) recvEnvelope.getMessage());
					break;
				case net.ib.mtalk.network.MethodId.Session.Notification.Select:
					onSelectResult((SelectRes) recvEnvelope.getMessage());
					break;
				}
			} catch (Exception e) {
				MTalkLogWriter.e(e);
			}
		}

		@Override
		public void onNetError(int resultCode, Object sentObject) {
			if (resultCode != NetResultReceiver.TCP_RESULT) {	// 그냥 예외 체크
				MTalkLogWriter.e("SelecterService.ServiceCallback.onNetError(): Should not reach here. It's not tcp result");
				return;
			}
			try {
				TcpEnvelope sentEnvelope = (TcpEnvelope) sentObject;
				// 네트워크 결과에 따른 분기
				switch (sentEnvelope.getMethodId()) {
				case net.ib.mtalk.network.MethodId.Session.Ping.Ping:
					onPingResult(null);
					break;
				case net.ib.mtalk.network.MethodId.Session.Notification.Select:
					onSelectResult(null);
					break;
				}
			} catch (Exception e) {
				MTalkLogWriter.e(e);
			}
		}

	}

	/*	네트워크 결과에 따른 페이즈 분기 끝	*/

	/*	페이즈 수행 시작	*/

	/**
	 * 현재의 모든 예약된 작업과 진행중인 작업을 중단하고 침묵한다.
	 * 일시적 중단이며 이후에 다시 플로우가 시작되는 것에는 관여하지 않는다.
	 */
	protected void onPhaseStop() {
		MTalkLogWriter.v("SelecterService.onPhaseStop() - STT");

		// 모든 푸시 스케줄을 취소
		stopSchedule(this, ACTION_STOP);
		stopSchedule(this, ACTION_START);
		stopSchedule(this, ACTION_CHECK);
		// 모든 푸시 콜백을 회수.
		if (mCallback != null) {
			mCallback.recall();
			mCallback = null;
		}
		// 부가 작업의 종료에 의한 서비스 동작을 철회
		SelectResultHandler.readyWork(mHandler);

		MTalkLogWriter.v("SelecterService.onPhaseStop() - END");
	}

	/**
	 * 푸시 등록 상황과 flowCondition에 따라, 시작 페이즈에서 해야 할 일을 한다.
	 */
	protected void onPhaseStart() {
		MTalkLogWriter.v("SelecterService.onPhaseStart() - STT");

		// 기존의 서비스 상태 체크.
		switch (getState()) {
		case INACTIVE:
			MTalkLogWriter.v("SelecterService.onPhaseStart(): Canceling potential garbages. Ready to start.");
			onPhaseStop();	// 이전 상태 클리어를 위해. 아마 안 해도 될 것이다.
			break;
		case ACTIVE:
			MTalkLogWriter.v("SelecterService.onPhaseStart() - END (Already on going)");
			return;
		case LONGWAIT:
			// 기다리는 도중 임의의 시점에 시작 요청이 들어왔다.
			// 모든 기다림을 중단하고 그냥 시작 요청을 받아들이자.
			MTalkLogWriter.v("SelecterService.onPhaseStart(): Canceling longWait. Ready to start.");
			onPhaseStop();
			onPhaseFinish();
			break;
		}
		setState(State.ACTIVE);

		// 주어진 정보와 지정된 행동양식에 따른 분기
		if (GlobalPreferences.isRegistrationIdLocated()) {	// 구글에 푸시 등록이 끝났고 우리 서버에도 푸시 등록이 끝나 있다.
			// C2DM이 되는 단말이다.
			MTalkLogWriter.d("SelecterService.onPhaseStart(): C2DM available");
			switch (getMode()) {
			case MONO_SELECT: // do nothing. 우리 서버가 푸시를 보내줄 수 있으므로 이 경우는 1셀렉트를 하면 안 된다.
				//TcpClient.close();
				onPhaseFinish();
				break;
			case NONE_SELECT: // do nothing. 명시적으로 아무것도 하지 말라고 지정받은 경우.
				onPhaseFinish();
				break;
			case FULL_SELECT: // 4 셀렉트를 걸고 모든 결과를 처리해야 한다.
				startPing();
				break;
			}
		} else {
			// C2DM을 기대할 수 없는 단말이다.
			MTalkLogWriter.d("SelecterService.onPhaseStart(): C2DM inavailable");
			switch (getMode()) {
			case MONO_SELECT: // 1 셀렉트를 걸고 결과를 처리해야 한다.
				startPing();
				break;
			case NONE_SELECT: // do nothing. 명시적으로 아무것도 하지 말라고 지정받은 경우.
				onPhaseFinish();
				break;
			case FULL_SELECT: // 4 셀렉트를 걸고 모든 결과를 처리해야 한다.
				startPing();
				break;
			}
		}

		MTalkLogWriter.v("SelecterService.onPhaseStart() - END");
	}

	/**
	 * 이 함수는 C2DM 을 수신한 경우에 Start 대신 불려야 한다.
	 */
	protected void onPhasePush() {
		MTalkLogWriter.v("SelecterService.onPhasePush() - STT");

		if (! GlobalPreferences.isRegistrationIdLocated()) {
			// C2DM을 기대할 수 없는 단말이다. 애초에 이 경우에는 이 함수가 호출되면 안 된다.
			MTalkLogWriter.e("SelecterService.onPhasePush(): Should Not Reach Here");
			return;
		}

		switch (getMode()) {
		case MONO_SELECT: // go on. 1 셀렉트를 하고 종료하면 된다.
			break;
		case NONE_SELECT: // do nothing. 명시적으로 아무것도 하지 말라고 지정받은 경우.
			MTalkLogWriter.v("SelecterService.onPhasePush() - END (NONE_SELECT)");
			return;
		case FULL_SELECT:	// do nothing. C2DM 푸시가 왔는데 이미 풀 셀렉트 모드이다? 건드리면 안 될 것 같다.
			MTalkLogWriter.v("SelecterService.onPhasePush() - END (FULL_SELECT)");
			return;
		}

		switch (getState()) {
		case INACTIVE:
			MTalkLogWriter.v("SelecterService.onPhasePush(): Canceling potential garbages. Ready to start.");
			onPhaseStop();	// 이전 상태 클리어를 위해. 아마 안 해도 될 것이다.
			break;
		case ACTIVE:
			MTalkLogWriter.v("SelecterService.onPhasePush() - END (Already on going)");
			return;
		case LONGWAIT:
			// 기다리는 도중 임의의 시점에 시작 요청이 들어왔다.
			// 모든 기다림을 중단하고 그냥 시작 요청을 받아들이자.
			MTalkLogWriter.v("SelecterService.onPhasePush(): Canceling longWait. Ready to start.");
			onPhaseStop();
			onPhaseFinish();
			break;
		}

		setState(State.ACTIVE);
		startPing();

		MTalkLogWriter.v("SelecterService.onPhasePush() - END");
	}

	/**
	 * 시작 페이즈에서 호출된다.
	 * 핑을 보낸다.
	 */
	protected void startPing() {
		MTalkLogWriter.v("SelecterService.startPing() - STT");

		Ping.ping("SelecterServiceStartPing".getBytes(), makeNewCallback());	// 핑을 날려본다. 얃! 이 때 접속이 성공한다면 로그인 시도도 함께 될 것이다.
		MTalkLogWriter.d("SelecterService.startPing(): ping by service");

		MTalkLogWriter.v("SelecterService.startPing() - END");
	}

	/**
	 * 핑의 결과를 처리한다.
	 * 성공적이라면 셀렉트 페이즈로 넘어간다.
	 * 문제가 있다면 기다렸다가 처음부터 재시도한다.
	 * @param pongRes 핑의 결과
	 */
	protected void onPingResult(PongRes pongRes) {
		MTalkLogWriter.v("SelecterService.onPingResult() - STT");

		if (pongRes == null) {
			onPhaseStop();
			onPhaseLongwait();
			MTalkLogWriter.v("SelecterService.onPingResult() - END (netError)");
			return;
		}
		switch (pongRes.getResponseCode()) {
		case Common.SUCCESS:
			onPhaseSelect();
			break;
		case Common.NOT_ENOUGH_PERMISSION:
		case Common.NOT_EXIST_SESSION:
		case Common.NOT_IMPLEMENTED:
		case Common.INTERNAL_ERROR:
		case Common.NOT_ALLOWED:
			// 그냥 조용히 죽자.
			MTalkLogWriter.w("SelecterService.onPingResult(): sleep silently with ResponseCode "+pongRes.getResponseCode());
			onPhaseStop();
			onPhaseFinish();
			break;
		default:
			onPhaseStop();
			onPhaseLongwait();
			break;
		}

		MTalkLogWriter.v("SelecterService.onPingResult() - END");
	}

	/**
	 * 푸시 등록 상황과 flowCondition에 따라, 셀렉트 페이즈에서 해야 할 일을 한다.
	 */
	protected void onPhaseSelect() {
		MTalkLogWriter.v("SelecterService.onPhaseSelect() - STT");

		if (GlobalPreferences.isRegistrationIdLocated()) {	// 구글에 푸시 등록이 끝났고 우리 서버에도 푸시 등록이 끝나 있다.
			// C2DM이 되는 단말이다.
			MTalkLogWriter.d("SelecterService.onPhaseSelect(): C2DM available");
			switch (getMode()) {
			case MONO_SELECT: // 여기에 들어오는 경우는 C2DM 푸시를 받은 경우다. 1회 수행하면 된다.
				monoSelect();
				break;
			case NONE_SELECT: // do nothing. 명시적으로 아무것도 하지 말라고 지정받은 경우.
				onPhaseFinish();
				break;
			case FULL_SELECT: // 4 셀렉트를 걸고 모든 결과를 처리해야 한다.
				fullSelect();
				break;
			}
		} else {
			// C2DM을 기대할 수 없는 단말이다.
			MTalkLogWriter.d("SelecterService.onPhaseSelect(): C2DM inavailable");
			switch (getMode()) {
			case MONO_SELECT: // 1셀렉트를 반복해서 걸고 결과를 처리해야 한다.
				monoSelect();
				break;
			case NONE_SELECT: // do nothing. 명시적으로 아무것도 하지 말라고 지정받은 경우.
				onPhaseFinish();
				break;
			case FULL_SELECT: // 4 셀렉트를 걸고 모든 결과를 처리해야 한다.
				fullSelect();
				break;
			}
		}

		MTalkLogWriter.v("SelecterService.onPhaseSelect() - END");
	}

	/**
	 * 셀렉트 페이즈에서 호출된다.
	 * 푸시 동작을 모사하기 위해 1셀렉트를 건다.
	 */
	protected void monoSelect() {
		MTalkLogWriter.v("SelecterService.monoSelect() - STT");

		if (! NetFlowControl.hasAuth()) {	// 인증이 완료되지 않았다면 이 페이즈는 시도될 수 없다.
			MTalkLogWriter.d("SelecterService.monoSelect(): has no Auth");
			onPhaseFinish();
		} else {
			// 노티 규격 세팅
			NotificationId notiId = new NotificationId();
			notiId.setMethod(Common.VIEW_CHANGED);
			notiId.setNotiUuid(GlobalPreferences.getLastViewChangedNotiUuid());
			// 요청 구성
			SelectReq req = new SelectReq();
			req.addNotiId(notiId);
			req.setTimeout((int)NetworkConstant.PUSH_POLL_TIME);
			// 발송
			Notification.select(req, makeNewCallback(), NetworkConstant.PUSH_POLL_TIME + NetworkConstant.Session.TIMEOUT_MILLIS);
			MTalkLogWriter.d("SelecterService.oneSelect(): 1-select by service");
			// 그리고 서버 응답이 오지 않거나 연결만 끊기는 경우를 대비하여 PUSH_CHECK_TIME 이후에 무조건 확인을 하도록 해야 한다.
			scheduleWork(this, ACTION_CHECK, NetworkConstant.PUSH_CHECK_TIME);
		}

		MTalkLogWriter.v("SelecterService.monoSelect() - END");
	}

	/**
	 * 셀릭트 페이즈에서 호출된다.
	 * 셀렉트를 걸 수 있는 모든 정보를 요청한다.
	 */
	protected void fullSelect() {
		MTalkLogWriter.v("SelecterService.fullSelect() - STT");

		if (! NetFlowControl.hasAuth()) {	// 인증이 완료되지 않았다면 이 페이즈는 시도될 수 없다.
			MTalkLogWriter.d("SelecterService.fullSelect(): has no Auth");
			onPhaseFinish();
		} else {
			// 노티 규격 세팅
			NotificationId buddyNotiId = new NotificationId();
			buddyNotiId.setMethod(Common.BUDDY_CHANGED);
			buddyNotiId.setNotiUuid(GlobalPreferences.getLastBuddyChangedNotiUuid());
			NotificationId viewNotiId = new NotificationId();
			viewNotiId.setMethod(Common.VIEW_CHANGED);
			viewNotiId.setNotiUuid(GlobalPreferences.getLastViewChangedNotiUuid());
			NotificationId messageNotiId = new NotificationId();
			messageNotiId.setMethod(Common.MESSAGE_RECEIVED);
			messageNotiId.setNotiUuid(GlobalPreferences.getLastMessageReceivedNotiUuid());
			NotificationId readmarkNotiId = new NotificationId();
			readmarkNotiId.setMethod(Common.READMARK_RECEIVED);
			readmarkNotiId.setNotiUuid(GlobalPreferences.getLastReadmarkReceivedNotiUuid());
			// 요청 구성
			SelectReq req = new SelectReq();
			req.addNotiId(buddyNotiId);
			req.addNotiId(viewNotiId);
			req.addNotiId(messageNotiId);
			req.addNotiId(readmarkNotiId);
			req.setTimeout((int)NetworkConstant.PUSH_POLL_TIME);
			// 발송
			Notification.select(req, makeNewCallback(), NetworkConstant.PUSH_POLL_TIME + NetworkConstant.Session.TIMEOUT_MILLIS);
			MTalkLogWriter.d("SelecterService.fullSelect(): 4-select by service");
			// 그리고 서버 응답이 오지 않거나 연결만 끊기는 경우를 대비하여 PUSH_CHECK_TIME 이후에 무조건 확인을 하도록 해야 한다.
			scheduleWork(this, ACTION_CHECK, NetworkConstant.PUSH_CHECK_TIME);
		}

		MTalkLogWriter.v("SelecterService.fullSelect() - END");
	}

	/**
	 * 셀렉트의 결과를 처리한다.
	 * 항목이 발견되면 CHECK의 예약을 해제하고, 항목에 해당하는 작업으로 분기한다.
	 * 문제가 있다면 기다렸다가 처음부터 재시도한다.
	 * @param selectRes 셀렉트의 결과
	 */
	protected void onSelectResult(SelectRes selectRes) {
		MTalkLogWriter.v("SelecterService.onSelectResult() - STT");

		if (selectRes == null) {
			onPhaseStop();
			onPhaseLongwait();
			MTalkLogWriter.v("SelecterService.onSelectResult() - END (netError)");
			return;
		}

		// 이 때는 이미 폴링 타임아웃이 되었거나 폴링이 타임아웃 전에 끝났거나.
		// 어느 쪽이든 ACTION_SELECT의 정상 종료시에는 셀렉트를 다시 하면 된다.
		switch(selectRes.getResponseCode()) {
		case Common.SUCCESS:	// 서버가 정상 리턴한 경우
			// 셀렉트 결과를 처리하자.
			NotificationInfo notificationInfo = null;
			NotificationId notiId = null;

			int notiCount = selectRes.getNotiInfoCount();
			if (notiCount == 0) {	// 그냥 시간이 다 돼서 리턴한 경우
				// 이 때는 후속 쿼리 체인이 만들어지지 않을 것이다. 다시 셀렉트를 걸거나 하면 됨.
				if (! GlobalPreferences.isRegistrationIdLocated()) {	// C2DM을 쓸 수 없는 상태이면
					// 셀렉트 페이즈의 정상 진행 분기의 마지막에서는 다시 셀렉트를 걸어줘야 한다.
					// 그러나 이 리턴은 너무 빨리 끝났을 가능성이 있다. 따라서 그냥 ACTION_CHECK를 기다린다.
				} else {	// C2DM을 쓸 수 있다면
					// 조용히 대기상태로 돌아간다.
					onPhaseStop();
					onPhaseFinish();
				}
			} else {	// 무언가 셀렉트 결과가 들어있었을 경우
				// 모든 항목의 처리가 끝난 후에 다음 주기의 시작을 결정해야 한다.
			}

			SelectResultHandler.readyWork(mHandler);
			for (int i=0; i<notiCount; ++i) {
				notificationInfo = selectRes.getNotiInfo(i);
				notiId = notificationInfo.getNotiId();
				switch (notiId.getMethod()) {
				case Common.BUDDY_CHANGED:
					SelectResultHandler.enqueWork(new BuddyChangeHandler(this, notificationInfo));
					break;
				case Common.VIEW_CHANGED:
					SelectResultHandler.enqueWork(new ViewChangeHandler(this, notificationInfo));
					break;
				case Common.MESSAGE_RECEIVED:
					SelectResultHandler.enqueWork(new MessageReceivedHandler(this, notificationInfo));
					break;
				case Common.READMARK_RECEIVED:
					SelectResultHandler.enqueWork(new ReadmarkReceivedHandler(this, notificationInfo));
					break;
				}
			}
			MTalkLogWriter.d("SelecterService.onSelectResult(): Launching SelectResultHandlers...");
			SelectResultHandler.launchAllWorks();
			break;
		case Common.CANCELED:
			/* 나의 다른 요청에 의해 캔슬된 select long polling 이다.
			 * 이미 이 호출이 이루어진 콜백은 recall 되었어야 한다.
			 * 따라서 이 위치에 도달할 수 없어야 하나, 만일 도달한다 해도 여기에서 딱히 해줄 작업은 없다.
			 * 이미 서비스는 자체 동작을 하고 있을 것이므로 방해하지 말자.
			 * */
			MTalkLogWriter.w("SelecterService.onSelectResult(): May not reach here.");
			MTalkLogWriter.w("SelecterService.onSelectResult(): do nothing with ResponseCode "+selectRes.getResponseCode());
			break;
		case Common.NOT_ENOUGH_PERMISSION:
		case Common.NOT_EXIST_SESSION:
		case Common.NOT_IMPLEMENTED:
		case Common.INTERNAL_ERROR:
		case Common.NOT_ALLOWED:
			// 그냥 조용히 죽자.
			MTalkLogWriter.w("SelecterService.onSelectResult(): finishing with ResponseCode "+selectRes.getResponseCode());
			onPhaseStop();
			onPhaseFinish();
			break;
		default:
			onPhaseStop();
			onPhaseLongwait();
			break;
		}

		MTalkLogWriter.v("SelecterService.onSelectResult() - END");
	}

	/**
	 * 지정한 시간 후에도 셀렉트의 리턴이 오지 않았을 때. 혹은 내용이 없는 셀렉트가 왔을 때 불려야 한다.
	 */
	protected void onPhaseCheck() {
		MTalkLogWriter.v("SelecterService.onPhaseCheck() - STT");

		onPhaseStop();
		onPhaseFinish();
		actionStart(getApplicationContext());//onPhaseStart();

		MTalkLogWriter.v("SelecterService.onPhaseCheck() - END");
	}

	/**
	 * 긴 타이머를 걸고 재시도를 기다릴 때 호출한다.
	 */
	protected void onPhaseLongwait() {
		MTalkLogWriter.v("SelecterService.onPhaseFinish() - STT");

		setState(State.LONGWAIT);
		scheduleWork(this, ACTION_START, NetworkConstant.PUSH_CHECK_TIME);	// 한참 있다가 재시도한다.

		if (GlobalPreferences.isRegistrationIdLocated()) {
			if (getMode() == Mode.MONO_SELECT) {
				NetFlowControl.trafficControl(getApplicationContext());
			}
		}

		MTalkLogWriter.v("SelecterService.onPhaseFinish() - END");
	}

	/**
	 * 서비스의 한 주기가 끝날 때 이 함수를 호출해야 한다.
	 */
	protected void onPhaseFinish() {
		MTalkLogWriter.v("SelecterService.onPhaseFinish() - STT");

		setState(SelecterService.State.INACTIVE);

		if (GlobalPreferences.isRegistrationIdLocated()) {
			if (getMode() == Mode.MONO_SELECT) {
				NetFlowControl.trafficControl(getApplicationContext());
			}
		}

		MTalkLogWriter.v("SelecterService.onPhaseFinish() - END");
	}

	/*	페이즈 수행 끝	*/

	/*	연결 부담 관리 시작	*/

	protected static long lastTraffilcTime = 0;

	/**
	 * 네트워크 관련 이벤트가 일어날 때 호출된다.
	 * 이전의 호출이 충분히 오래 전이었고, 현재 앱이 백그라운드이면, 네트워크를 끊어버리자.
	 */
	protected void onNetworkTraffic() {
		lastTraffilcTime = android.os.SystemClock.elapsedRealtime();	// 시간을 저장하고
		scheduleWork(this, ACTION_COLLECT, NetworkConstant.Session.TIMEOUT_MILLIS);	// 알람을 다시 예약한다. 이전의 알람은 취소된다.
	}
	
	/**
	 * 네트워크 작업이 idle 상태일 때 연결을 끊어버린다.
	 * 서버의 부담을 줄이거나 연결 관리를 일관되게 만듬.
	 * @param myTraffilcTime 이전에 이 작업을 예약할 때 저장했던 시간.
	 */
	protected void onNetworkCollect(long myTraffilcTime) {
		if (getMode() != Mode.FULL_SELECT) { // 백그라운드일 때
			if (lastTraffilcTime >= myTraffilcTime) {
				switch (getState()) {
				case ACTIVE:
					MTalkLogWriter.v("SelecterService.onNetworkCollect(): network busy with service.");
					break;
				case INACTIVE:
					MTalkLogWriter.i("SelecterService.onNetworkCollect(): network idle. closing");
					TcpClient.lockClose();	// 그냥 close() 메소드를 쓰면 호출이 무한히 재귀 -> 스택 오버플로우.
					break;
				case LONGWAIT:
					MTalkLogWriter.i("SelecterService.onNetworkCollect(): network longwait. closing");
					TcpClient.lockClose();	// 그냥 close() 메소드를 쓰면 호출이 무한히 재귀 -> 스택 오버플로우.
					break;
				}
			} else {
				MTalkLogWriter.v("SelecterService.onNetworkCollect(): network busy at background.");
			}
		} else {
			MTalkLogWriter.v("SelecterService.onNetworkCollect(): network busy at forground.");
		}
		System.gc();
	}

	/*	연결 부담 관리 끝	*/

}
